{-----------------------------------------------------------------------------
 Unit Name: frmCodeExplorer
 Author:    Kiriakos Vlahos
 Date:      09-Mar-2005
 Purpose:   Code Explorer Window
 History:
-----------------------------------------------------------------------------}

unit frmCodeExplorer;

interface

uses
  Winapi.Windows,
  System.UITypes,
  System.Classes,
  System.ImageList,
  System.Generics.Defaults,
  System.Generics.Collections,
  System.JSON,
  Vcl.Controls,
  Vcl.ExtCtrls,
  Vcl.Menus,
  Vcl.ImgList,
  Vcl.VirtualImageList,
  JvComponentBase,
  JvDockControlForm,
  JvAppStorage,
  VirtualTrees.Types,
  VirtualTrees.BaseTree,
  VirtualTrees.BaseAncestorVCL,
  VirtualTrees.AncestorVCL,
  VirtualTrees,
  TB2Item,
  SpTBXItem,
  frmIDEDockWin,
  SynEditTypes,
  XLSPTypes,
  SynEditLsp;

type
  TCESortOrder = (soPosition, soAlpha);
  TCEExpandState = (esUnknown, esExpanded, esCollapsed);

  TCodeBlock = record
    StartLine: Integer;
    EndLine: Integer;
  end;

  TAbstractCENodeClass = class of TAbstractCENode;
  TAbstractCENode = class
  private
    FChildren: TObjectList<TAbstractCENode>;
    function GetChildCount: Integer;
    function GetChildren(Index: Integer): TAbstractCENode;
    class var SortOrder: TCESortOrder;
    class var NodeComparer: IComparer<TAbstractCENode>;
    class var FileName: string;
  protected
    FName: string;
    FCodePos: TBufferCoord;
    FExpanded: TCEExpandState;
    FNode: PVirtualNode;
    function GetHint: string; virtual;
    function GetCaption: string; virtual;
    function GetImageIndex: Integer; virtual;
  public
    class constructor Create;
    constructor CreateFromSymbol(Symbol: TLSPDocumentSymbol); virtual;
    destructor Destroy; override;
    function AddChild(CENode: TAbstractCENode): Integer;
    procedure Sort(ASortOrder: TCESortOrder);
    property Name: string read FName;
    property CodePos: TBufferCoord read FCodePos;
    property Hint: string read GetHint;
    property Caption: string read GetCaption;
    property ImageIndex: Integer read GetImageIndex;
    property ChildCount: Integer read GetChildCount;
    property Children[Index: Integer]: TAbstractCENode read GetChildren;
    property Expanded: TCEExpandState read FExpanded write FExpanded;
  end;

  TGroupCENode = class abstract(TAbstractCENode)
  end;

  TCodeElementCENode = class(TAbstractCENode)
  private
    FCodeBlock: TCodeBlock;
  protected
    function GetHint: string; override;
  public
    constructor CreateFromSymbol(Symbol: TLSPDocumentSymbol); override;
    function GetScopeForLine(LineNo: Integer): TCodeElementCENode;
    property CodeBlock: TCodeBlock read FCodeBlock;
  end;

  TImportCENode = class(TAbstractCENode)
  protected
    function GetHint: string; override;
    function GetImageIndex: Integer; override;
  end;

  TImportsCENode = class(TGroupCENode)
  protected
    function GetHint: string; override;
    function GetCaption: string; override;
  end;

  TGlobalsCENode = class(TGroupCENode)
    function GetHint: string; override;
    function GetCaption: string; override;
  end;

  TModuleCENode = class(TCodeElementCENode)
  private
    FOffsetXY: TPoint;
    FImports: TImportsCENode;
    FGlobals: TGlobalsCENode;
  protected
    function GetHint: string; override;
    function GetImageIndex: Integer; override;
  public
    constructor CreateFromSymbols(const AFileName: string; Symbols:
        TList<TLSPDocumentSymbol>);
    property OffsetXY: TPoint read FOffsetXY write FOffsetXY;
    property Imports: TImportsCENode read FImports;
    property Globals: TGlobalsCENode read FGlobals;
  end;

  TVariableCENode = class(TAbstractCENode)
  end;

  TGlobalCENode = class(TVariableCENode)
  protected
    function GetHint: string; override;
    function GetImageIndex: Integer; override;
  end;

  TAttributesCENode = class(TGroupCENode)
  protected
    function GetHint: string; override;
    function GetCaption: string; override;
  end;

  TClassCENode = class(TCodeElementCENode)
  private
    FAttributes: TAttributesCENode;
  protected
    function GetImageIndex: Integer; override;
  public
    constructor CreateFromSymbol(Symbol: TLSPDocumentSymbol); override;
  end;

  TAttributeCENode = class(TVariableCENode)
  protected
    function GetHint: string; override;
    function GetImageIndex: Integer; override;
  end;

  TFunctionCENode = class(TCodeElementCENode)
  protected
    function GetImageIndex: Integer; override;
  public
    constructor CreateFromSymbol(Symbol: TLSPDocumentSymbol); override;
  end;

  TMethodCENode = class(TFunctionCENode)
  protected
    function GetImageIndex: Integer; override;
  end;

  TCEUpdateReason = (ceuSymbolsChanged, ceuEditorEnter);

  TCodeExplorerWindow = class(TIDEDockWindow)
    Panel1: TPanel;
    ExplorerTree: TVirtualStringTree;
    CEPopupMenu: TSpTBXPopupMenu;
    mnExpandAll: TSpTBXItem;
    nCollapseAll: TSpTBXItem;
    SpTBXSeparatorItem1: TSpTBXSeparatorItem;
    mnShowSelection: TSpTBXItem;
    CENodePopUpMenu: TSpTBXPopupMenu;
    mnHighlight: TSpTBXItem;
    mnFindDefinition: TSpTBXItem;
    SpTBXSeparatorItem2: TSpTBXSeparatorItem;
    mnFindReferences: TSpTBXItem;
    mnAlphaSort: TSpTBXItem;
    mnFollowEditor: TSpTBXItem;
    vilCodeImages: TVirtualImageList;
    vilImages: TVirtualImageList;
    procedure ExplorerTreeGetHint(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; var LineBreakStyle: TVTTooltipLineBreakStyle;
      var HintText: string);
    procedure FormCreate(Sender: TObject);
    procedure ExplorerTreeInitNode(Sender: TBaseVirtualTree; ParentNode,
      Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
    procedure ExplorerTreeGetImageIndex(Sender: TBaseVirtualTree;
      Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
      var Ghosted: Boolean; var ImageIndex: TImageIndex);
    procedure ExplorerTreeInitChildren(Sender: TBaseVirtualTree;
      Node: PVirtualNode; var ChildCount: Cardinal);
    procedure ExplorerTreeDblClick(Sender: TObject);
    procedure mnExpandAllClick(Sender: TObject);
    procedure nCollapseAllClick(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure ExplorerTreeKeyPress(Sender: TObject; var Key: Char);
    procedure ExplorerTreeChange(Sender: TBaseVirtualTree; Node: PVirtualNode);
    procedure ExplorerTreeContextPopup(Sender: TObject; MousePos: TPoint;
      var Handled: Boolean);
    procedure mnHighlightClick(Sender: TObject);
    procedure mnFindDefinitionClick(Sender: TObject);
    procedure mnFindReferencesClick(Sender: TObject);
    procedure mnAlphaSortClick(Sender: TObject);
    procedure ExplorerTreeCollapsed(Sender: TBaseVirtualTree;
      Node: PVirtualNode);
    procedure ExplorerTreeExpanded(Sender: TBaseVirtualTree;
      Node: PVirtualNode);
    procedure ExplorerTreeScroll(Sender: TBaseVirtualTree; DeltaX,
      DeltaY: TDimension);
    procedure mnFollowEditorClick(Sender: TObject);
    procedure ExplorerTreeGetCellText(Sender: TCustomVirtualStringTree;
      var E: TVSTGetCellTextEventArgs);
  private
    const FBasePath = 'Code Explorer Options'; // Used for storing settings
    var FFileId: string;
    FModuleNode: TModuleCENode;
    procedure NavigateToNodeElement(Node: PVirtualNode;
      ForceToMiddle: Boolean = True; Activate: Boolean = True);
    procedure UpdateModuleNode(const FileId: string; Symbols:
        TList<TLSPDocumentSymbol>);
    procedure UpdateTree(const FileId: string;
      UpdateReason: TCEUpdateReason; NewModuleNode: TModuleCENode);
  public
    // AppStorage
    procedure StoreSettings(AppStorage: TJvCustomAppStorage); override;
    procedure RestoreSettings(AppStorage: TJvCustomAppStorage); override;

    procedure ClearAll;
    procedure ShowEditorCodeElement;
    procedure UpdateWindow(DocSymbols: TDocSymbols; UpdateReason: TCEUpdateReason);
    class function CreateInstance: TIDEDockWindow; override;
  end;

var
  CodeExplorerWindow: TCodeExplorerWindow;

implementation

uses
  System.SysUtils,
  System.Math,
  System.IOUtils,
  System.Threading,
  Vcl.Forms,
  JvGnugettext,
  SynEdit,
  dmResources,
  dmCommands,
  frmPyIDEMain,
  uEditAppIntfs,
  uCommonFunctions,
  cLSPClients,
  cPyScripterSettings;

{$R *.dfm}

procedure TCodeExplorerWindow.UpdateTree(const FileId: string;
      UpdateReason: TCEUpdateReason; NewModuleNode: TModuleCENode);
begin
  if GI_PyIDEServices.IsClosing then Exit;

  var ActiveEditor := GI_PyIDEServices.ActiveEditor;
  if ActiveEditor = nil then
  begin
    ExplorerTree.Clear;
    FFileId := '';
    FModuleNode := nil;
    Exit;
  end;

  var ShowingActiveEditor := SameFileName(ActiveEditor.FileId, FFileId);
  if not ShowingActiveEditor then
    FFileId := ActiveEditor.FileId;

  var Editor: IEditor;
  if SameFileName(FFileId, FileId) then
    Editor := ActiveEditor
  else
    Editor := GI_EditorFactory.GetEditorByFileId(FileId);

  var SameModule := ShowingActiveEditor and (Editor = ActiveEditor);

  if UpdateReason = ceuSymbolsChanged then
  begin
    if Assigned(Editor) then
    begin
      FreeAndNil(TDocSymbols(Editor.DocSymbols).ModuleNode);
      TDocSymbols(Editor.DocSymbols).ModuleNode := NewModuleNode;
    end
    else
    begin
      // Editor must have been destroyed
      NewModuleNode.Free;
      Exit;
    end;

    if ShowingActiveEditor and (ActiveEditor <> Editor) then
      // A non active editor's DocSymbols have been updated
      Exit;
  end;
  FModuleNode := TDocSymbols(ActiveEditor.DocSymbols).ModuleNode as TModuleCENode;

  // Turn off Animation to speed things up
  ExplorerTree.TreeOptions.AnimationOptions :=
    ExplorerTree.TreeOptions.AnimationOptions - [toAnimatedToggle];

  if SameModule then
  begin
    if FModuleNode = nil then
     ExplorerTree.Clear
    else
    begin
      ExplorerTree.RootNodeCount := 1;
      // The same module but changed
      // ReInit the tree with the new data to keep it as close as possible
      if mnAlphaSort.Checked then
        FModuleNode.Sort(soAlpha);
      ExplorerTree.BeginUpdate;
      try
        ExplorerTree.ReinitNode(nil, True, True);
        ExplorerTree.Invalidate;
      finally
        ExplorerTree.EndUpdate;
      end;
    end;
  end
  else
  begin
    ExplorerTree.Clear;
    if FModuleNode <> nil then
    begin
      ExplorerTree.RootNodeCount := 1;
      ExplorerTree.OffsetXY := FModuleNode.OffsetXY;
      ExplorerTree.ValidateNode(nil, True);
      //ExplorerTree.Refresh;
    end;
  end;
  ExplorerTree.TreeOptions.AnimationOptions :=
    ExplorerTree.TreeOptions.AnimationOptions + [toAnimatedToggle];
  ShowEditorCodeElement;
end;

procedure TCodeExplorerWindow.FormActivate(Sender: TObject);
begin
  inherited;
  if ExplorerTree.CanFocus then
    ExplorerTree.SetFocus;
end;

procedure TCodeExplorerWindow.FormCreate(Sender: TObject);
begin
  ImageName := 'CodeExplorer';
  inherited;
  // Let the tree know how much data space we need.
  ExplorerTree.NodeDataSize := SizeOf(Pointer);
end;

procedure TCodeExplorerWindow.ExplorerTreeInitNode(Sender: TBaseVirtualTree;
  ParentNode, Node: PVirtualNode;
  var InitialStates: TVirtualNodeInitStates);
var
  CENode: TAbstractCENode;
begin
  if ParentNode = nil then
    CENode := FModuleNode
  else begin
    var ParentCENode := ParentNode.GetData<TAbstractCENode>;
    CENode := ParentCENode.Children[Node.Index];
  end;
  Node.SetData<TAbstractCENode>(CENode);

  if CENode.ChildCount > 0 then
  begin
    Include(InitialStates, ivsHasChildren);
    if (ivsReInit in InitialStates) and not (CENode is TModuleCENode) then
    begin
      if vsExpanded in Node.States then
        CENode.Expanded := esExpanded
      else
        CENode.Expanded := esCollapsed;
    end
    else if CENode.Expanded = esExpanded then
      Include(InitialStates, ivsExpanded);
  end;
  // reverse link from CENode to Tree node
  CENode.FNode := Node;
end;

procedure TCodeExplorerWindow.ExplorerTreeKeyPress(Sender: TObject;
  var Key: Char);
begin
  if Key = Char(VK_RETURN) then
    NavigateToNodeElement(ExplorerTree.GetFirstSelected);
end;

procedure TCodeExplorerWindow.ExplorerTreeScroll(Sender: TBaseVirtualTree;
  DeltaX, DeltaY: TDimension);
begin
  if Assigned(FModuleNode) then
    FModuleNode.OffsetXY := ExplorerTree.OffsetXY;
end;

procedure TCodeExplorerWindow.ExplorerTreeInitChildren(
  Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal);
begin
  ChildCount := Node.GetData<TAbstractCENode>.ChildCount;
end;

procedure TCodeExplorerWindow.ExplorerTreeGetImageIndex(
  Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind;
  Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: TImageIndex);
begin
  if Kind in [ikNormal, ikSelected] then
    ImageIndex := Node.GetData<TAbstractCENode>.ImageIndex;
end;

procedure TCodeExplorerWindow.ExplorerTreeGetCellText(
  Sender: TCustomVirtualStringTree; var E: TVSTGetCellTextEventArgs);
begin
  E.CellText := E.Node.GetData<TAbstractCENode>.Caption;
end;

procedure TCodeExplorerWindow.ExplorerTreeGetHint(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex;
  var LineBreakStyle: TVTTooltipLineBreakStyle; var HintText: string);
begin
  HintText := Node.GetData<TAbstractCENode>.Hint;
end;

class function TCodeExplorerWindow.CreateInstance: TIDEDockWindow;
begin
  CodeExplorerWindow := TCodeExplorerWindow.Create(Application);
  Result := CodeExplorerWindow;
end;

procedure TCodeExplorerWindow.ExplorerTreeChange(Sender: TBaseVirtualTree;
  Node: PVirtualNode);
begin
  if Assigned(Node) and (vsSelected in Node.States) and
    mnShowSelection.Checked
  then
   NavigateToNodeElement(Node, True, False);
end;

procedure TCodeExplorerWindow.ExplorerTreeCollapsed(Sender: TBaseVirtualTree;
  Node: PVirtualNode);
begin
  if Assigned(Node) then
    Node.GetData<TAbstractCENode>.Expanded := esCollapsed;
end;

procedure TCodeExplorerWindow.ExplorerTreeContextPopup(Sender: TObject;
  MousePos: TPoint; var Handled: Boolean);
var
  Node: PVirtualNode;
  CENode: TAbstractCENode;
  PopUpMenu: TPopupMenu;
  Pos: TPoint;
  HitInfo: THitInfo;
begin
  if (MousePos.X = -1) and (MousePos.Y = -1) then
    // Keyboard invocation
    Node := ExplorerTree.GetFirstSelected
  else begin
    ExplorerTree.GetHitTestInfoAt(MousePos.X, MousePos.Y, True, HitInfo);
    Node := HitInfo.HitNode;
    if not ([hiOnItemLabel, hiOnNormalIcon] * HitInfo.HitPositions <> []) then
      Node := nil;
    if Assigned(Node) and not (vsSelected in Node.States) then
      Node := nil;
  end;
  if Assigned(Node) then begin
    PopUpMenu := CENodePopUpMenu;
    //UpdatePopupActions;
    CENode := Node.GetData<TAbstractCENode>;
    mnFindDefinition.Enabled :=
      (CENode is TCodeElementCENode) and not (CENode is TModuleCENode) or
      (CENode is TVariableCENode) or (CENode is TImportCENode);
    mnFindReferences.Enabled := mnFindDefinition.Enabled;
    mnHighlight.Enabled := mnFindDefinition.Enabled;
  end else
    PopUpMenu := CEPopupMenu;

  Pos := ExplorerTree.ClientToScreen(MousePos);
  PopUpMenu.Popup(Pos.X, Pos.Y);
  Handled := True;
end;

procedure TCodeExplorerWindow.ExplorerTreeDblClick(Sender: TObject);
begin
  NavigateToNodeElement(ExplorerTree.HotNode);
end;

procedure TCodeExplorerWindow.ExplorerTreeExpanded(Sender: TBaseVirtualTree;
  Node: PVirtualNode);
begin
  if Assigned(Node) then
    Node.GetData<TAbstractCENode>.Expanded := esExpanded;
end;

procedure TCodeExplorerWindow.UpdateModuleNode(const FileId: string;
  Symbols: TList<TLSPDocumentSymbol>);
var
  ModuleNode: TModuleCENode;
begin
    if Assigned(Symbols) then
    begin
      ModuleNode := TModuleCENode.CreateFromSymbols(FileId, Symbols);
      Symbols.Free;
    end
    else
      ModuleNode := nil;
    TThread.ForceQueue(nil, procedure
      begin
        UpdateTree(FileId, ceuSymbolsChanged, ModuleNode);
      end);
end;

procedure TCodeExplorerWindow.UpdateWindow(DocSymbols: TDocSymbols;
  UpdateReason: TCEUpdateReason);
var
  Symbols: TList<TLSPDocumentSymbol>;
  LFileId: string;
begin
  Assert(Assigned(DocSymbols), 'TCodeExplorerWindow.UpdateWindow');
  LFileId := DocSymbols.FileId;
  case UpdateReason of
    ceuSymbolsChanged:
      begin
        DocSymbols.Lock;
        try
          if DocSymbols.Symbols = nil then
          begin
            Assert(GetCurrentThreadId = MainThreadID, 'TCodeExplorerWindow.UpdateWindow');
            if FFileId = LFileId then
            begin
              ExplorerTree.Clear;
              FModuleNode := nil;
            end;
            FreeAndNil(DocSymbols.ModuleNode);
          end
          else
          begin
            // Called from a thread <> MainThread
            Symbols := DocSymbols.Symbols; // Will destroyed in UpdateModuleNode
            DocSymbols.Symbols := nil;
            var Task := TTask.Create(procedure
              begin
                UpdateModuleNode(LFileId, Symbols);
              end);
            Task.Start;
          end;
        finally
          DocSymbols.Unlock;
        end;
      end;
    ceuEditorEnter:
      begin
        TThread.ForceQueue(nil, procedure
          begin
            if not GI_PyIDEServices.IsClosing then
              UpdateTree(LFileId, ceuEditorEnter, nil);
          end);
      end;
  end;
end;

procedure TCodeExplorerWindow.StoreSettings(AppStorage: TJvCustomAppStorage);
begin
  inherited;
  AppStorage.WriteBoolean(FBasePath+'\AlphaSort', mnAlphaSort.Checked);
  AppStorage.WriteBoolean(FBasePath+'\Show Selection', mnShowSelection.Checked);
  AppStorage.WriteBoolean(FBasePath+'\Follow Editor', mnFollowEditor.Checked);
end;

procedure TCodeExplorerWindow.ClearAll;
begin
  ExplorerTree.Clear;
end;

procedure TCodeExplorerWindow.ShowEditorCodeElement;
var
  Editor: IEditor;
  CodeElement: TCodeElementCENode;
begin
  if not mnFollowEditor.Checked then Exit;

  Editor := GI_PyIDEServices.ActiveEditor;
  if not Assigned(Editor) then Exit;

  if Assigned(FModuleNode) and (FFileId = Editor.FileId) and
    (ExplorerTree.RootNodeCount > 0)
  then
  begin
    CodeElement := FModuleNode.GetScopeForLine(Editor.ActiveSynEdit.CaretY);
    if Assigned(CodeElement) and Assigned(CodeElement.FNode) then
    begin
      ExplorerTree.TreeOptions.AnimationOptions :=
        ExplorerTree.TreeOptions.AnimationOptions - [toAnimatedToggle];
      ExplorerTree.OnChange := nil;
      ExplorerTree.FullyVisible[CodeElement.FNode] := True;
      ExplorerTree.Selected[CodeElement.FNode] := True;
      ExplorerTree.ScrollIntoView(CodeElement.FNode, False);
      ExplorerTree.OnChange := ExplorerTreeChange;
      ExplorerTree.TreeOptions.AnimationOptions :=
        ExplorerTree.TreeOptions.AnimationOptions + [toAnimatedToggle];
    end;
  end;
end;

procedure TCodeExplorerWindow.NavigateToNodeElement(Node: PVirtualNode;
      ForceToMiddle: Boolean = True; Activate: Boolean = True);
var
  CENode: TAbstractCENode;
  CodePos: TBufferCoord;
  Len: Integer;
  Editor: IEditor;
begin
  CodePos.Line := - 1;
  Len := 0;

  if Assigned(Node) then
  begin
    CENode := Node.GetData<TAbstractCENode>;
    if not (CENode is TGroupCENode) then
    begin
      CodePos := CENode.CodePos;
      if not (CENode is TModuleCENode) then
        Len := Length(CENode.Name);
    end else if Assigned(ExplorerTree.GetFirstChild(Node)) then begin
        NavigateToNodeElement(Node.FirstChild, ForceToMiddle, Activate);
        Exit;
    end else
      Exit;

    Editor := GI_PyIDEServices.ActiveEditor;
    if Assigned(Editor) and (Editor.FileId = FFileId) and (CodePos.Line >= 0) then
    begin
      with Editor.ActiveSynEdit do
      begin
        CaretXY := CodePos;
        EnsureCursorPosVisibleEx(ForceToMiddle);
        if Len > 0 then
          BlockEnd := BufferCoord(BlockBegin.Char + Len, BlockBegin.Line);
      end;
      if Activate then Editor.Activate(False);
    end;
  end;
end;

procedure TCodeExplorerWindow.mnAlphaSortClick(Sender: TObject);
begin
  if Assigned(FModuleNode) then begin
    if mnAlphaSort.Checked then
      FModuleNode.Sort(soAlpha)
    else
      FModuleNode.Sort(soPosition);

    ExplorerTree.BeginUpdate;
    try
      ExplorerTree.ReinitNode(nil, True, True);
      ExplorerTree.Invalidate;
    finally
      ExplorerTree.EndUpdate;
    end;
  end;
end;

procedure TCodeExplorerWindow.mnExpandAllClick(Sender: TObject);
begin
  ExplorerTree.FullExpand;
end;

procedure TCodeExplorerWindow.mnFindDefinitionClick(Sender: TObject);
var
  Node: PVirtualNode;
begin
  Node := ExplorerTree.GetFirstSelected;
  if Assigned(Node) then
    NavigateToNodeElement(Node);
end;

procedure TCodeExplorerWindow.mnFindReferencesClick(Sender: TObject);
begin
  var Node := ExplorerTree.GetFirstSelected;
  if Assigned(Node) then begin
    var CENode := Node.GetData<TAbstractCENode>;
    if (CENode is TCodeElementCENode) and not (CENode is TModuleCENode) or
       (CENode is TVariableCENode) or (CENode is TImportCENode) then
    begin
      NavigateToNodeElement(Node);
      PyIDEMainForm.actFindReferencesExecute(Self);
    end;
  end;
end;

procedure TCodeExplorerWindow.mnFollowEditorClick(Sender: TObject);
begin
  if mnFollowEditor.Checked then
    ShowEditorCodeElement;
end;

procedure TCodeExplorerWindow.mnHighlightClick(Sender: TObject);
begin
  var Node := ExplorerTree.GetFirstSelected;
  if Assigned(Node) then begin
    var CENode := Node.GetData<TAbstractCENode>;
    if (CENode is TCodeElementCENode) and not (CENode is TModuleCENode) or
       (CENode is TVariableCENode) or (CENode is TImportCENode)
    then
      CommandsDataModule.HighlightWordInActiveEditor(CENode.Name);
  end;
end;

procedure TCodeExplorerWindow.nCollapseAllClick(Sender: TObject);
begin
  ExplorerTree.FullCollapse;
end;

procedure TCodeExplorerWindow.RestoreSettings(AppStorage: TJvCustomAppStorage);
begin
  inherited;
  mnAlphaSort.Checked := AppStorage.ReadBoolean(FBasePath+'\AlphaSort', False);
  mnShowSelection.Checked := AppStorage.ReadBoolean(FBasePath+'\Show Selection', True);
  mnFollowEditor.Checked := AppStorage.ReadBoolean(FBasePath+'\Follow Editor', True);
end;

{ TAbstractCENode }

function TAbstractCENode.AddChild(CENode: TAbstractCENode): Integer;
begin
  if FChildren = nil then
    FChildren := TObjectList<TAbstractCENode>.Create(True);

  Result := FChildren.Add(CENode);
end;

class constructor TAbstractCENode.Create;
begin
  NodeComparer := TComparer<TAbstractCENode>.Construct(
     function(const Nd1, Nd2: TAbstractCENode): Integer
     begin
       if Nd1 is TImportsCENode then
         Result := -1
       else if Nd2 is TImportsCENode then
         Result := 1
       else if Nd1 is TGlobalsCENode then
         Result := -1
       else if Nd2 is TGlobalsCENode then
         Result := 1
       else if Nd1 is TAttributesCENode then
         Result := -1
       else if Nd2 is TAttributesCENode then
         Result := 1
       else
       begin
         if SortOrder = soAlpha then
           Result := ComparePythonIdents(Nd1.Name, Nd2.Name)
         else begin
           Result := Sign(Nd1.CodePos.Line -
                            Nd2.CodePos.Line);
           if Result = 0 then
             Result := Sign(Nd1.CodePos.Char -
                              Nd2.CodePos.Char);
         end;
       end;
     end);

end;

constructor TAbstractCENode.CreateFromSymbol(Symbol: TLSPDocumentSymbol);
begin
  inherited Create;
  FName := Symbol.name;
  FCodePos.Line := Symbol.selectionRange.start.line;
  Inc(FCodePos.Line);
  FCodePos.Char := Symbol.selectionRange.start.character;
  Inc(FCodePos.Char);
end;

function TAbstractCENode.GetChildren(Index: Integer): TAbstractCENode;
begin
  if Assigned(FChildren) then
    Result := FChildren[Index]
  else
    Result := nil;
end;

function TAbstractCENode.GetHint: string;
begin
  Result := '';
end;

function TAbstractCENode.GetImageIndex: Integer;
begin
  Result := -1;
end;

procedure TAbstractCENode.Sort(ASortOrder: TCESortOrder);
var
  Child: Pointer;
begin
  SortOrder := ASortOrder;
  if not Assigned(FChildren) then Exit;

  for Child in FChildren do
    TAbstractCENode(Child).Sort(SortOrder);

  FChildren.Sort(NodeComparer);
end;

destructor TAbstractCENode.Destroy;
begin
  FreeAndNil(FChildren);
  inherited;
end;

function TAbstractCENode.GetCaption: string;
begin
  Result := FName;
end;

function TAbstractCENode.GetChildCount: Integer;
begin
  if Assigned(FChildren) then
    Result := FChildren.Count
  else
    Result := 0;
end;

{ TModuleCENode }

function TModuleCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Python);
end;

constructor TModuleCENode.CreateFromSymbols(const AFileName: string;
  Symbols: TList<TLSPDocumentSymbol>);
var
  Kind: TLSPSymbolKind;
  NodeClass: TAbstractCENodeClass;
  Node: TAbstractCENode;
begin
  inherited Create;
  FileName := AFileName;
  FName := TPath.GetFileName(AFileName);
  FExpanded := esExpanded;
  FCodePos := BufferCoord(1, 1);
  FCodeBlock.StartLine := 1;
  FCodeBlock.EndLine := MaxInt;

  for var Symbol in Symbols do
  begin
    Kind := Symbol.kind;

    case Kind of
      TLSPSymbolKind.symModule: NodeClass := TImportCENode;
      TLSPSymbolKind.symClass: NodeClass := TClassCENode;
      TLSPSymbolKind.symFunction: NodeClass := TFunctionCENode;
      TLSPSymbolKind.symVariable: NodeClass := TGlobalCENode;
    else
      Continue;
    end;
    try
      Node := NodeClass.CreateFromSymbol(Symbol);
    except
      Continue;
    end;
    if NodeClass = TImportCENode then
    begin
      if FImports = nil then
      begin
        FImports := TImportsCENode.Create;
        var Index := AddChild(FImports);
        FChildren.Move(Index, 0);
      end;
      FImports.AddChild(Node);
    end
    else if NodeClass = TGlobalCENode then
    begin
      if FGlobals = nil then
      begin
        FGlobals := TGlobalsCENode.Create;
        var Index := AddChild(FGlobals);
        if FImports <> nil then
          FChildren.Move(Index, 1)
        else
          FChildren.Move(Index, 0);
      end;
      FGlobals.AddChild(Node);
    end
    else
      AddChild(Node);
  end;
end;

function TModuleCENode.GetHint: string;
begin
  Result := Format(_('Python Module "%s"'), [Name]);
end;

{ TImportsCENode }

function TImportsCENode.GetCaption: string;
begin
  Result := _('Imports');
end;

function TImportsCENode.GetHint: string;
begin
  Result := _('Imported modules');
end;

{ TImportCENode }

function TImportCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Module);
end;

function TImportCENode.GetHint: string;
begin
//  if ModuleImport.RealName <> ModuleImport.Name then
//    Result := Format(_('Imported Module "%s" as %s at line %d'),
//                    [ModuleImport.RealName, ModuleImport.Name,
//                     fCodeElement.CodePos.LineNo])
//  else
    Result := Format(_('Imported Module "%s" at line %d'),
                    [Name, CodePos.Line]);
end;

{ TGlobalsCENode }

function TGlobalsCENode.GetCaption: string;
begin
  Result := 'Globals';
end;

function TGlobalsCENode.GetHint: string;
begin
  Result := _('Global variables');
end;

{ TGlobalCENode }

function TGlobalCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Variable);
end;

function TGlobalCENode.GetHint: string;
begin
  Result := Format(_('Global variable "%s" defined at line %d'),
                    [Caption, CodePos.Line]);
end;

{ TClassCENode }

function TClassCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Klass);
end;

constructor TClassCENode.CreateFromSymbol(Symbol: TLSPDocumentSymbol);
var
  Kind: TLSPSymbolKind;
  Symbols: TLSPDocumentSymbols;
  NodeClass: TAbstractCENodeClass;
  Node: TAbstractCENode;
begin
  inherited;

  Symbols := Symbol.children;

  for var CE in Symbols do
  begin
    Kind := CE.kind;

    case Kind of
      TLSPSymbolKind.symClass: NodeClass := TClassCENode;
      TLSPSymbolKind.symMethod: NodeClass := TMethodCENode;
      TLSPSymbolKind.symFunction: NodeClass := TFunctionCENode;
      TLSPSymbolKind.symProperty,
      TLSPSymbolKind.SymVariable: NodeClass := TAttributeCENode;
    else
      Continue;
    end;
    try
      Node := NodeClass.CreateFromSymbol(CE);
    except
      Continue;
    end;
    if NodeClass = TAttributeCENode then
    begin
      if FAttributes = nil then
      begin
        FAttributes := TAttributesCENode.Create;
        var Index := AddChild(FAttributes);
        FChildren.Move(Index, 0);
      end;
      FAttributes.AddChild(Node);
    end
    else
      AddChild(Node);
  end;
  if PyIDEOptions.ExplorerInitiallyExpanded and (ChildCount > 0) then
   FExpanded := esExpanded;
end;

{ TAttributesCENode }

function TAttributesCENode.GetCaption: string;
begin
  Result := _('Attributes');
end;

function TAttributesCENode.GetHint: string;
begin
  Result := _('Class attributes');
end;

{ TAttributeCENode }

function TAttributeCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Field);
end;

function TAttributeCENode.GetHint: string;
begin
  Result := Format(_('Class attribute "%s" defined at line %d'),
                    [Caption, CodePos.Line]);
end;

{ TFunctionCENode }

constructor TFunctionCENode.CreateFromSymbol(Symbol: TLSPDocumentSymbol);
var
  Kind: TLSPSymbolKind;
  Symbols: TLSPDocumentSymbols;
  NodeClass: TAbstractCENodeClass;
  Node: TAbstractCENode;
begin
  inherited;

  Symbols := Symbol.children;

  for var CE in Symbols do
  begin
    Kind := CE.kind;

    case Kind of
      TLSPSymbolKind.symClass: NodeClass := TClassCENode;
      TLSPSymbolKind.symFunction: NodeClass := TFunctionCENode;
    else
      Continue;
    end;
    try
      Node := NodeClass.CreateFromSymbol(CE);
    except
      Continue;
    end;
    AddChild(Node);
  end;
end;

function TFunctionCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Func);
end;

{ TMethodCENode }

function TMethodCENode.GetImageIndex: Integer;
begin
  Result := Integer(TCodeImages.Method);
end;

{ TCodeElementCENode }

constructor TCodeElementCENode.CreateFromSymbol(Symbol: TLSPDocumentSymbol);
begin
  inherited CreateFromSymbol(Symbol);
  FCodeBlock.StartLine := Symbol.range.start.line;
  Inc(FCodeBlock.StartLine);
  FCodeBlock.EndLine := Symbol.range.&end.line;
  Inc(FCodeBlock.EndLine);
end;

function TCodeElementCENode.GetScopeForLine(LineNo: Integer): TCodeElementCENode;
begin
  if InRange(LineNo, CodeBlock.StartLine, CodeBlock.EndLine) then begin
    Result := Self;
    //  try to see whether the line belongs to a child
    if not Assigned(FChildren) then Exit;
    for var I := 0 to FChildren.Count - 1 do begin
      var Node := Children[I];
      if Node is TCodeElementCENode then
      begin
        var CENode := TCodeElementCENode(Node);
        // recursive call
        CENode := CENode.GetScopeForLine(LineNo);
        if Assigned(CENode) then
        begin
          Result := CENode;
          Break;
        end;
      end;
    end;
  end else
    Result := nil;
end;

function TCodeElementCENode.GetHint: string;
begin
  Result := TPyLspClient.SimpleHintAtCoordinates(FileName, CodePos);
  Result := StringReplace(Result, '<br>', SLineBreak, [rfReplaceAll]);
end;


initialization
  TIDEDockWindow.RegisterDockWinClass(ideCodeExplorer, TCodeExplorerWindow);
end.

